import * as events from 'events'
import * as stream from 'stream'
import * as jschardet from 'jschardet'
import * as iconv from 'iconv-lite'
import os from 'os'
import child, { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'child_process'
import platform from "../platform";
import process from "process";

export class CommandInvoker extends events.EventEmitter {
    constructor(command: string, args?: string[], options?: CommandOptions) {
        super()

        this.command = command
        this.args = args || []
        this.options = options || {}
        this.spawn = child.spawn
    }

    setSpawn(spawn: (command: string,args?: ReadonlyArray<string>, options?: SpawnOptionsWithoutStdio) => ChildProcessWithoutNullStreams) {
        this.spawn = spawn
    }

    private readonly command: string
    private readonly args: string[]
    private readonly options: CommandOptions
    private spawn: (command: string,args?: ReadonlyArray<string>, options?: SpawnOptionsWithoutStdio) => ChildProcessWithoutNullStreams

    private stdoutBuffer: Buffer = Buffer.alloc(0)
    private stderrBuffer: Buffer = Buffer.alloc(0)
    private detectedEncoding = 'utf8'
    private isEncodingDetected = false

    private aggregateData(
        data: Buffer,
        restData: Buffer,
        onLineData: OnLineFunc
    ): Buffer {
        try {
            // 如果是linux，强制使用 UTF-8 编码
            if (platform.PLATFORM_LINUX === platform.getPlatform()) {
                // 支持指定参数 fallback 回检测逻辑
                const flowDetectedEncoding = process.env['FLOW_RUNNER_DETECTED_ENCODING']
                if (flowDetectedEncoding === undefined || flowDetectedEncoding === "") {
                    this.detectedEncoding = 'utf8'
                    this.isEncodingDetected = true
                }
            }

            const concatBuffer: Buffer = Buffer.concat([restData, data])
            if (concatBuffer.length < 128 && !this.isEncodingDetected) {
                // 如果长度小于128, 且没有检测到编码，那么先不做聚合
                return concatBuffer
            }
            let content = this.bufferToString(concatBuffer)
            let eolIndex = content.indexOf(os.EOL)

            while (eolIndex > -1) {
                const line = content.substring(0, eolIndex)
                onLineData(line)

                content = content.substring(eolIndex + os.EOL.length)
                eolIndex = content.indexOf(os.EOL)
            }

            return this.stringToBuffer(content)
        } catch (err) {
            return Buffer.from('')
        }
    }

    private bufferToString(data: Buffer): string {
        if (this.isEncodingDetected && this.detectedEncoding.length != 0) {
            return iconv.decode(data, this.detectedEncoding)
        }
        const iDetectedMap = jschardet.detect(data);
        let detectedEncoding = iDetectedMap.encoding
        if (detectedEncoding == null || detectedEncoding == 'ascii') {
            // fallback to utf8
            detectedEncoding = 'utf8'
        } else if (detectedEncoding == 'windows-1252') {
            detectedEncoding = 'GB2312'
        }

        this.isEncodingDetected = true
        this.detectedEncoding = detectedEncoding
        // 只有检测到非 utf8 编码时才输出以下日志，减少干扰
        if (detectedEncoding !== 'utf8') {
            console.log(` +-- Detected Encoding: ${detectedEncoding} --+`)
            console.log(` +-- Detected Confidence: ${iDetectedMap.confidence} --+`)
        }
        return iconv.decode(data, this.detectedEncoding)
    }

    private stringToBuffer(content: string): Buffer {
        return iconv.encode(content, this.detectedEncoding)
    }

    async callCommand(): Promise<number> {
        return new Promise<number>((resolve, reject) => {
            if (this.options.outStream) {
                this.options.outStream.write(this.getCommandString() + os.EOL)
            }

            const commandProcess = this.spawn(this.command, this.args, this.options)
            if (commandProcess.stdout) {
                commandProcess.stdout.on('data', (data: Buffer) => {
                    if (this.options.listener && this.options.listener.stdout) {
                        this.options.listener.stdout(data)
                    }

                    if (this.options.outStream) {
                        this.options.outStream.write(data)
                    }

                    this.stdoutBuffer = this.aggregateData(
                        data,
                        this.stdoutBuffer,
                        (line: string) => {
                            if (
                                this.options.listener &&
                                this.options.listener.stdoutLine
                            ) {
                                this.options.listener.stdoutLine(line)
                            }
                        }
                    )
                })
            }

            if (commandProcess.stderr) {
                commandProcess.stderr.on('data', (data: Buffer) => {
                    if (this.options.listener && this.options.listener.stderr) {
                        this.options.listener.stderr(data)
                    }

                    if (this.options.errStream) {
                        this.options.errStream.write(data)
                    }

                    this.stderrBuffer = this.aggregateData(
                        data,
                        this.stderrBuffer,
                        (line: string) => {
                            if (
                                this.options.listener &&
                                this.options.listener.stderrLine
                            ) {
                                this.options.listener.stderrLine(line)
                            }
                        }
                    )
                })
            }

            const onErrorFunc = (err: Error): void => {
                this.flushBuffer()
                reject(err)
            }

            const onFinishFunc = (code: number): void => {
                this.flushBuffer()
                resolve(code)
            }

            commandProcess.on('error', onErrorFunc)
            commandProcess.on('exit', onFinishFunc)
            commandProcess.on('close', onFinishFunc)
        })
    }

    private flushBuffer() {
        if (
            this.stderrBuffer.length > 0 &&
            this.options.listener &&
            this.options.listener.stderrLine
        ) {
            const lines = this.bufferToLines(this.stderrBuffer)
            for (const line of lines) {
                this.options.listener.stderrLine(line)
            }
            this.stderrBuffer = iconv.encode('', this.detectedEncoding)
        }

        if (
            this.stdoutBuffer.length > 0 &&
            this.options.listener &&
            this.options.listener.stdoutLine
        ) {
            const lines = this.bufferToLines(this.stdoutBuffer)
            for (const line of lines) {
                this.options.listener.stdoutLine(line)
            }
            this.stdoutBuffer = iconv.encode('', this.detectedEncoding)
        }
    }

    private bufferToLines(buffer: Buffer): string[] {
        const stdoutContent = this.bufferToString(buffer)
        return stdoutContent.split(/\r?\n/)
    }

    private getCommandString(): string {
        let cmdStr = ''
        cmdStr += this.command
        for (let i = this.args.length - 1; i >= 0; i--) {
            cmdStr += ` ${this.args[i]}`
        }

        return cmdStr
    }
}

export class CommandOptions {
    outStream?: stream.Writable
    errStream?: stream.Writable
    listener?: CommandListener
    cwd?: string
}

export interface CommandResult {
    exitCode: number
    stdoutContent: string
    stderrContent: string
}

export interface CommandListener {
    stdout?: DataHandlerFunc
    stderr?: DataHandlerFunc

    stdoutLine?: LineHandlerFunc
    stderrLine?: LineHandlerFunc
}

export type DataHandlerFunc = (data: Buffer) => void
export type LineHandlerFunc = (line: string) => void
export type OnLineFunc = (line: string) => void
